iT邦幫忙

2022 iThome 鐵人賽

DAY 18
1
Web 3

從以太坊白皮書理解 web 3 概念系列 第 19

從以太坊白皮書理解 web 3 概念 - Day18

  • 分享至 

  • xImage
  •  

從以太坊白皮書理解 web 3 概念 - Day18

Learn Solidity - Day 10 - Build an Oracle

今天將會透過 Lession 14: Build an Oracle 練習製作 Oracle Contract

Oracle 介紹

假設今天要製作一個去中心化金融應用,這個應用讓使用者能從 Contract 把等值於美金換成 1 Ether。

為了要完成這個這個應用,應用 Contract 必須要能知道當下 1 Ether 值多少美金。

要查詢當下 Ether 對應到美金的匯率可以透過 Biance 交易所的 API 去查

然而, Contract 本身無法直接與外界 api 做互動

所以需要一個 Oracle 來當作外界資料來源。

Oracle 是 Smart Contract 用來與外界呼叫的一種機制。

其流程如下

實作

  1. 建立一個資料夾 mkdir EthPriceOracle ; 然後進入資料夾
  2. 使用以下指令初始化專案
npm init -y

or

yarn init -y
  1. 安裝以下套件:
    truffle, openzeppelin-solidity, loom-js, loom-truffle-provider, bn.js, axios
npm i truffle openzeppelin-solidity loom-js loom-truffle-provider bn.js axios

or

yarn add truffle openzeppelin-solidity loom-js loom-truffle-provider bn.js axios
  1. 建立 oracle 資料夾
mkdir oracle

在 oracle 內初始化 Truffle

cd oracle; npx truffle init; cd ..
  1. 建立 caller 資料夾
mkdir caller

在 caller 內初始化 Truffle

cd caller; npx truffle init; cd ..

檢查資料夾結構透過 tree 指令如下

tree -L 2 -I node_modules
.
├── caller
│   ├── contracts
│   ├── migrations
│   ├── test
│   └── truffle-config.js
├── oracle
│   ├── contracts
│   ├── migrations
│   ├── test
│   └── truffle-config.js
└── package.json

呼叫其他的 Contract

在開始實作 Oracle Contract 之前

首先要來看一下呼叫 Oracle Contract 的 Caller Contract

為了要能與Oracle Contract 互動,Caller Contract 必須要俱備以下資訊

  • Oracle Contract 的 address
  • 要呼叫 Oracle Contract function 的 function Signature 或者說介面

已知到一旦 Oracle Contract 一發佈到鏈上,要更新就只能重新發佈

為了能夠讓 Caller Contract 能夠在 Oracle Contract 更新時不被影響到要重新發佈

需要透過建立個 function 來更新 Oracle Contrat 的 address

實作 CallerContract

  1. 宣告一個變數 address oracleAddress 並且設定為 private
address private oracleAddress;
  1. 宣告一個 function setOracleInstanceAddress
    需要一個參數: _oracleInstanceAddress(address)
    設定存取權限為: public
  2. 更新 function setOracleInstanceAddress 第一行內容如下
oracleAddress = _oracleInstanceAddress;
pragma solidity 0.5.0;
contract CallerContract {
    // start here
    address private oracleAddress;
    function setOracleInstanceAddress(address _oracleInstanceAddress) public {
        oracleAddress =  _oracleInstanceAddress;
    }
}

實作呼叫 Oracle Contract

定義呼叫介面

介面類似於 Contract,但只是用來宣告 functions signature 卻不實作。且所有 function 接需要為 external

舉例來說:假設有一個 FastFood Contract

如下

pragma solidity 0.5.0;

contract FastFood {
  function makeSandwich(string calldata _fillingA, string calldata _fillingB) external {
    //Make the sandwich
  }
}

假設需要從另一個 PrepareLunch Contract 呼叫 makeSandSwich 就必須要定一個介面 FastFoodInterface.sol

如下

pragma solidity 0.5.0;

interface FastFoodInterface {
   function makeSandwich(string calldata _fillingA, string calldata _fillingB) external;
}

然後在 PrepareLaunch 內引入介面

加入初始化的 FastFoodInterface 邏輯

fastFoodInstance = FastFoodInterface(_address);

然後就可以使用 fastFoodInstance 來呼教 makeSandwich

pragma solidity 0.5.0;
import "./FastFoodInterface.sol";

contract PrepareLunch {

  FastFoodInterface private fastFoodInstance;

  function instantiateFastFoodContract (address _address) public {
    fastFoodInstance = FastFoodInterface(_address);
    fastFoodInstance.makeSandwich("sliced ham", "pickled veggies");
  }
}

實作

  1. 建立 caller/EthPriceOracleInterface.sol
  2. 在 caller/CallerContract.sol import caller/EthPriceOracleInterface.sol
  3. 宣告 EthPriceOracleInterface private oracleInstance;
  4. 初始化 oracleInstance = EthPriceOracleInterface(oracleAddress);
pragma solidity 0.5.0;
//1. Import from the "./EthPriceOracleInterface.sol" file
import "./EthPriceOracleInterface.sol";
contract CallerContract {
  // 2. Declare `EthPriceOracleInterface`
  EthPriceOracleInterface private oracleInstance;
  address private oracleAddress;
  function setOracleInstanceAddress (address _oracleInstanceAddress) public {
    oracleAddress = _oracleInstanceAddress;
    //3. Instantiate `EthPriceOracleInterface`
    oracleInstance = EthPriceOracleInterface(oracleAddress);
  }
}

宣告 onlyOwner modifier 在 setOracleInstanceAddress

前面 setOracleInstanceAddress 因為設定成 public

代表其他 Contract 可以任意更改 Oracle Address

因此需要使用 OpenZepelin's Ownable Contract 來限制呼叫者限定只有 owner 才能去做執行

實作

  1. import "openzeppelin-solidity/contracts/ownership/Ownable.sol"
  2. 讓 CallerContract 繼承 Ownable
  3. 在 setOracleInstanceAddress 加入 onlyOnwer
  4. 建立一個 event newOracleAddressEvent
    然後在 setOracleInstanceAddress 最後一行
    加入 emit newOracleAddressEvent(oracleAddress);
pragma solidity 0.5.0;
import "./EthPriceOracleInterface.sol";
// 1. import the contents of "openzeppelin-solidity/contracts/ownership/Ownable.sol"
import "openzeppelin-solidity/contracts/ownership/Ownable.sol";
contract CallerContract is Ownable { // 2. Make the contract inherit from `Ownable`
    EthPriceOracleInterface private oracleInstance;
    address private oracleAddress;
    event newOracleAddressEvent(address oracleAddress);
    // 3. On the next line, add the `onlyOwner` modifier to the `setOracleInstanceAddress` function definition
    function setOracleInstanceAddress (address _oracleInstanceAddress) public onlyOwner {
      oracleAddress = _oracleInstanceAddress;
      oracleInstance = EthPriceOracleInterface(oracleAddress);
      // 4. Fire `newOracleAddressEvent`
      emit newOracleAddressEvent(oracleAddress);
    }
}

使用 mapping 來紀錄 request

接下來將講解 Oracle 更新 ETH 的價格流程

當 ETH 價格更新, Smart Contract 會呼叫 Oracle Contract s內 getLatestEthPrice 功能。

然而,因為是非同步的, getLatestEthPrice 並不會直接回傳該值。

取而代之,會回傳一個 request id 。

然後 Oracle Contract 會透過 Biance API 讀取當下的 ETH 價格,然後執行 Caller Contract 對外的 callback function

Caller Contract 透過 callback function 取得最新的 ETH 價格

Mapping

每個 Dapp 的使用者都可以透過呼叫 Caller Contract 來發起更新 ETH 價格的 Request 。

因為 Caller Contract 無法在被呼叫時來即時處理這這些 Request 所以必須要使用一個結構來紀錄這些還沒有回應的 Request 。

這樣才能夠在之後取得當下 ETH 價格後,讓 callback 找到對應的 Request 作為回應。

可以使用一個叫作 myRequest 的 mapping 用來紀錄每個 requestID 對應的 Request 是否有回應過

實作

  1. 宣告一個 mapping 叫作 myRequests 如下
mapping(uint256 => bool) myRequests;
  1. 宣告一個 function updateEthPrice,
    不需要任何參數,並且存取權限是 public
  2. function updateEthPrice 第一行需要去呼叫 oracleInstance.getLatestEthPrice
    並且把回傳值存在 uint256 id 內
  3. 設定 myRequests[id] = true;
  4. emit ReceivedNewRequestIdEvent(id)
pragma solidity 0.5.0;
import "./EthPriceOracleInterface.sol";
import "openzeppelin-solidity/contracts/ownership/Ownable.sol";
contract CallerContract is Ownable {
    EthPriceOracleInterface private oracleInstance;
    address private oracleAddress;
    mapping(uint256=>bool) myRequests;
    event newOracleAddressEvent(address oracleAddress);
    event ReceivedNewRequestIdEvent(uint256 id);
    function setOracleInstanceAddress (address _oracleInstanceAddress) public onlyOwner {
      oracleAddress = _oracleInstanceAddress;
      oracleInstance = EthPriceOracleInterface(oracleAddress);
      emit newOracleAddressEvent(oracleAddress);
    }
    // Define the `updateEthPrice` function
    function updateEthPrice() public {
        uint256 id = oracleInstance.getLatestEthPrice();
        myRequests[id] = true;
        emit ReceivedNewRequestIdEvent(id);
    }
}

實作 CallBack function

呼叫 Binance API 是非同步的操作。所以 Caller Smart Contract 需要提供一個 callback function 來讓 Oracle Contract 呼叫。

以下是 callback 流程解說

  1. 一開始,會需要確認傳入的 id 是正確的
    所以需要使用 require 來做這件事
  2. 當確認 id 是正確後
    使用以下語法刪除原本在 myRequest內的值
delete myRequest[id];
  1. 最後透過 event 更新到前端的資料

實作 callback

  1. 建立一個 function callback
    需要兩個參數: _ethPrice(uint256), _id(uint256)
  2. 在 Contract 第一行 ,宣告一個 uint256 變數 ethPrice 並且設定讀取權限為 private
uint256 ethPrice private;
  1. 建立一個 event PriceUpdatedEvent
    並且需要兩個參數: ethPrice(uint256), id(uint256)
event PriceUpdatedEvent(uint256 ethPrice, uint256 id);
  1. 在 callback function 首先需要確認 myRequests[id] 是 true。會需要使用 require 語法如下
require(myRequests[id], "This request is not in my pending list.");
  1. 把新的 ETH price 存入 ethPrice 這個變數
  2. 最後把 id 從 myRequests 移除
delete myRequests[id];
  1. emit PriceUpdatedEvent
pragma solidity 0.5.0;
import "./EthPriceOracleInterface.sol";
import "openzeppelin-solidity/contracts/ownership/Ownable.sol";
contract CallerContract is Ownable {
    // 1. Declare ethPrice
    uint256 private ethPrice;
    EthPriceOracleInterface private oracleInstance;
    address private oracleAddress;
    mapping(uint256=>bool) myRequests;
    event newOracleAddressEvent(address oracleAddress);
    event ReceivedNewRequestIdEvent(uint256 id);
    // 2. Declare PriceUpdatedEvent
    event PriceUpdatedEvent(uint256 ethPrice, uint256 id);
    function setOracleInstanceAddress (address _oracleInstanceAddress) public onlyOwner {
        oracleAddress = _oracleInstanceAddress;
        oracleInstance = EthPriceOracleInterface(oracleAddress);
        emit newOracleAddressEvent(oracleAddress);
    }
    function updateEthPrice() public {
        uint256 id = oracleInstance.getLatestEthPrice();
        myRequests[id] = true;
        emit ReceivedNewRequestIdEvent(id);
    }
    function callback(uint256 _ethPrice, uint256 _id) public {
        // 3. Continue here
        require(myRequests[_id], "This request is not in my pending list.");
        ethPrice = _ethPrice;
        delete myRequests[_id];
        emit PriceUpdatedEvent(_ethPrice, _id);
    }
}

onlyOracle Modifier

在實作完 callback function 之後

必須要確保只有 Oracale Contract 才能去呼叫

可以透過檢查 msg.sender 是否等於 oracleAddress 來實作一個 onlyOracle modifier

實作 onlyOracle Modifier

  1. 宣告 modifer onlyOracle 並且放到 callback 之上
  2. 第一行需要加以下邏輯來限定只有 Oracle Contract 可以呼叫
require(msg.sender == oracleAddress, "You are not authorized to call this function.");
  1. 最後放入 _; 代表繼續執行其他邏輯
pragma solidity 0.5.0;
import "./EthPriceOracleInterface.sol";
import "openzeppelin-solidity/contracts/ownership/Ownable.sol";
contract CallerContract is Ownable {
    uint256 private ethPrice;
    EthPriceOracleInterface private oracleInstance;
    address private oracleAddress;
    mapping(uint256=>bool) myRequests;
    event newOracleAddressEvent(address oracleAddress);
    event ReceivedNewRequestIdEvent(uint256 id);
    event PriceUpdatedEvent(uint256 ethPrice, uint256 id);
    function setOracleInstanceAddress (address _oracleInstanceAddress) public onlyOwner {
      oracleAddress = _oracleInstanceAddress;
      oracleInstance = EthPriceOracleInterface(oracleAddress);
      emit newOracleAddressEvent(oracleAddress);
    }
    function updateEthPrice() public {
      uint256 id = oracleInstance.getLatestEthPrice();
      myRequests[id] = true;
      emit ReceivedNewRequestIdEvent(id);
    }
    function callback(uint256 _ethPrice, uint256 _id) public onlyOracle {
      require(myRequests[_id], "This request is not in my pending list.");
      ethPrice = _ethPrice;
      delete myRequests[_id];
      emit PriceUpdatedEvent(_ethPrice, _id);
    }
    modifier onlyOracle() {
      // Start here
      require(msg.sender == oracleAddress, "You are not authorized to call this function.");
      _;
    }
}

實作 getLatestEthPrice

getLatestEthPrice function

為了讓呼叫者可以紀錄 Request, getLatestEthPrice 首先需要計算 request id 還有為了避免 id 被人盜用,這個 id 必須很難預測。

為了難以預測也許會需要使用隨機數

隨機數的作法可以透過 kecca256 加入時間戳 now 與 randNounce 來實作如下

uint randNonce = 0;
uint modulus = 1000;
uint randomNumber = uint(keccak256(abi.encodePacked(now, msg.sender, randNonce))) % modulus;

這個隨機數產生了 0 - 999 之間的隨機數。

實作

  1. 宣告 function getLatestEthPrice
    回傳值: uint256
    存取權限: public
function getLatestEthPrice() public returns(uint256) {
	
}
  1. 在第一行把 randNonce++
  2. 計算一個 0 到 modulus 的隨機數,並且把結果存到一個 uint id
pragma solidity 0.5.0;
import "openzeppelin-solidity/contracts/ownership/Ownable.sol";
import "./CallerContractInterface.sol";
contract EthPriceOracle is Ownable {
  uint private randNonce = 0;
  uint private modulus = 1000;
  mapping(uint256=>bool) pendingRequests;
  event GetLatestEthPriceEvent(address callerAddress, uint id);
  event SetLatestEthPriceEvent(uint256 ethPrice, address callerAddress);
  // Start here
  function getLatestEthPrice() public returns(uint256) {
    randNonce++;
    uint id = uint(keccak256(abi.encodePacked(now, msg.sender, randNonce))) % modulus;
  }
}

處理 pendingRequest

接下來必須實作一個簡單的系統來紀錄 pending request

可以透過 mapping 來紀錄這些 request

最後 getLatestEthPrice 會發送一個 event 來通知該 request id

實作處理 pendingRequest 邏輯

  1. 建立 mapping(uint => bool) pendingRequests
  2. 首先更新 pendingRequests[id]=true
  3. emit GetLatestEthPriceEvent(msg.sender, id)
  4. 最後回傳 id
pragma solidity 0.5.0;
import "openzeppelin-solidity/contracts/ownership/Ownable.sol";
import "./CallerContractInterface.sol";
contract EthPriceOracle is Ownable {
  uint private randNonce = 0;
  uint private modulus = 1000;
  mapping(uint256=>bool) pendingRequests;
  event GetLatestEthPriceEvent(address callerAddress, uint id);
  event SetLatestEthPriceEvent(uint256 ethPrice, address callerAddress);
  function getLatestEthPrice() public returns (uint256) {
    randNonce++;
    uint id = uint(keccak256(abi.encodePacked(now, msg.sender, randNonce))) % modulus;

    // Start here
    pendingRequests[id] = true;
    emit GetLatestEthPriceEvent(msg.sender, id);
    return id;
  }
}

實作 setLatestEthPrice

當透過 js 元件取得 ETH 價格後 ,最後會呼叫 setLatestEthPrice 這個 function 來傳送結果

setLatestEthPrice 主要會有以下參數

  • ETH price
  • Caller 的 Contract Address
  • Request 的 id

首先必須要確認這個 function 只有 owner 可以呼叫

然後確認 request id 是否合法

如果合法最後要從 pendingRequests 移除掉 id

實作

  1. 建立 public function setLatestEthPrice
    需要3個參數: _ethPrice(uint256), _callerAddress(address), _id(uint256)
    並且需要設定只有 owner 可以呼叫
  2. 使用以下語法確認 _id 是合法的
require(pendingRequests[_id], "This request is not in my pending list.");
  1. 使用以下語法把 id 從 pendingRequests 中移除
delete pendingRequests[id];
pragma solidity 0.5.0;
import "openzeppelin-solidity/contracts/ownership/Ownable.sol";
import "./CallerContractInterface.sol";
contract EthPriceOracle is Ownable {
  uint private randNonce = 0;
  uint private modulus = 1000;
  mapping(uint256=>bool) pendingRequests;
  event GetLatestEthPriceEvent(address callerAddress, uint id);
  event SetLatestEthPriceEvent(uint256 ethPrice, address callerAddress);
  function getLatestEthPrice() public returns (uint256) {
    randNonce++;
    uint id = uint(keccak256(abi.encodePacked(now, msg.sender, randNonce))) % modulus;
    pendingRequests[id] = true;
    emit GetLatestEthPriceEvent(msg.sender, id);
    return id;
  }
  // Start here
  function setLatestEthPrice(uint256 _ethPrice, address _callerAddress, uint256 _id) public onlyOwner {
    require(pendingRequests[_id], "This request is not in my pending list.");
    delete pendingRequests[_id];
  }
}

呼叫 callback

接下來還有以下流程需要處理

  • 初始化 CallerContractInstance
  • 透過 CallerContractInstance 呼叫 callback 並且傳入新的 ETH 價格與 Request id
  • 最後發送一個 event 通知前端 price 已經成功更新

實作呼叫 callback 邏輯

  1. 建立 CallerContractInterface 變數 callerContractInstance
  2. 使用 Caller Contract address 來初始化 callerContractInstance
  3. 執行 callerContractInstance.callback 傳入 _ethPrice 與 _id
  4. emit SetLatestEthPriceEvent(_ethPrice, _callerAddress)
pragma solidity 0.5.0;
import "openzeppelin-solidity/contracts/ownership/Ownable.sol";
import "./CallerContractInterface.sol";
contract EthPriceOracle is Ownable {
  uint private randNonce = 0;
  uint private modulus = 1000;
  mapping(uint256=>bool) pendingRequests;
  event GetLatestEthPriceEvent(address callerAddress, uint id);
  event SetLatestEthPriceEvent(uint256 ethPrice, address callerAddress);
  function getLatestEthPrice() public returns (uint256) {
    randNonce++;
    uint id = uint(keccak256(abi.encodePacked(now, msg.sender, randNonce))) % modulus;
    pendingRequests[id] = true;
    emit GetLatestEthPriceEvent(msg.sender, id);
    return id;
  }
  function setLatestEthPrice(uint256 _ethPrice, address _callerAddress,   uint256 _id) public onlyOwner {
    require(pendingRequests[_id], "This request is not in my pending list.");
    delete pendingRequests[_id];
    // Start here
    CallerContractInterface callerContractInstance;
    callerContractInstance = CallerContractInterface(_callerAddress);
    callerContractInstance.callback(_ethPrice, _id);
    emit SetLatestEthPriceEvent(_ethPrice, _callerAddress);
  }
}


上一篇
從以太坊白皮書理解 web 3 概念 - Day17
下一篇
從以太坊白皮書理解 web 3 概念 - Day19
系列文
從以太坊白皮書理解 web 3 概念32
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言